Conversation
- gm-api.spec.ts: Phase 2 重启 context 后等待 service worker 注册完成 再交给 fixtures,避免 extensionId fixture 用 10s 全局超时等待失败 - gm-api.spec.ts: 用事件驱动 Promise 替换 500ms 轮询循环, console 结果一出现立即继续 - utils.ts: installScriptByCode 用 DOM 事件等待替代固定延迟: click 后等光标出现,粘贴后等 .view-lines 内容变化 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
新增 AI Agent 聊天模块,包括聊天界面、服务层、数据存储和多语言支持
将 Agent API 请求路由到 runtime/gmApi 通道,通过 @PermissionVerify.API 装饰器实现权限验证,脚本首次调用时弹窗确认,防止未授权消耗 API Token。
- 新增 OPFS 文件浏览器页面,支持目录浏览、文件预览和删除操作 - 默认进入 agents/ 目录,支持面包屑导航到根目录 - 支持在 conversation.create 时注册工具,简化多轮对话的工具传递 - 添加 8 种语言的 i18n 翻译
- 修复 callLLM 提前转发 done 事件导致客户端过早 resolve 的问题 - 修复 WindowMessage.connect 未使用 "*" targetOrigin 导致沙箱消息被丢弃 - 修复 tool calling 循环中 assistant 消息缺少 tool_calls 字段 - 支持 OpenAI 和 Anthropic 格式的 tool_calls 消息构建 - UI 合并 tool 结果到 assistant 的 ToolCallBlock 展示,过滤 tool/system 消息 - 提取 buildInstance 函数解决装饰器 this 绑定问题,使用 uuidv4 生成 ID
- ChatMessage 类型新增 usage/durationMs/firstTokenMs 字段 - Service Worker 测量 LLM 调用总耗时并通过 done 事件返回 - UI 层捕获首 token 延迟和 token 用量,持久化到消息中 - 新增 MessageToolbar 组件:左侧操作按钮(复制/重新回答/删除),右侧元数据信息(token用量/耗时/TTFT/工具调用数) - 流式期间显示实时计时动画,完成后切换为最终数据 - 删除操作支持二次确认 - 新增 deleteMessages 批量删除辅助函数 - 添加 i18n 多语言支持(8个locale)
- 新增 CATTool 元数据解析、存储、注册和执行完整链路 - 新增 CAT.agent.tools GM API(install/remove/list/call) - 修复 CATTool 沙箱执行时 args 未定义的问题:使用 compileScriptCodeByResource 包裹代码以启用 with 上下文绑定 - 优化 Agent Chat UI 组件样式
将 App.tsx (~1130行) 按职责拆分为: - utils.ts: 纯工具函数和常量 - hooks.tsx: useInstallData() 自定义 hook - components/CATToolInstallView.tsx: CATTool 安装视图 - components/ScriptInstallView.tsx: UserScript/Subscribe 安装视图 - App.tsx: 精简为路由分发 (~57行)
新增 example/agents/tools/ 目录: - hello_world.js: 最简 CATTool 示例 - text_processor.js: 多参数和 enum 类型 - json_formatter.js: JSON 处理和错误处理 - weather_query.js: GM_xmlhttpRequest 网络请求 - use_cattool.js: 通过脚本 API 安装和调用 CATTool - README.md: 格式说明和测试方法
- 修复历史消息未携带 toolCalls 字段导致多轮 tool calling 上下文丢失 - 添加 OpenAI 流式 API 错误响应处理 - 添加 /new 命令和 clearMessages 接口用于清空对话上下文 - UI 发送消息时正确传递 toolCallId 和 toolCalls
- 删除独立的 handleChat,UI 和 Sandbox 统一走 conversationChat 通道 - 持久化责任从 UI 移交到 SW,简化前端逻辑 - 字段命名统一:createdAt/updatedAt → createtime/updatetime - 支持 UI 动态切换 modelId - 大幅补充 Agent 相关测试(流式解析、tool calling 循环、CATTool 边界等)
- 对话实例支持 / 命令拦截机制,内置 /new 命令清空对话 - 脚本可通过 commands 选项注册自定义命令并覆盖内置命令 - 修复会话列表删除确认弹框因鼠标移开导致隐藏的问题 - 会话 ID 同步到 URL 参数,刷新页面保持当前会话选中 - 新增命令机制 8 个单元测试
- CATTool 安装改为打开安装页面确认,支持脚本来源追踪和更新提示 - CATToolRepo 索引/数据分离(tools.json 索引 + data/<id>.json 完整记录) - CATTool UUID 由 SW 统一生成,通过全局映射支持 GM API 权限验证 - AgentModelConfig 从 SystemConfig 迁移到独立的 AgentModelRepo - AgentModelRepo.getDefaultModelId 使用 chrome.storage.local 直接存取 - cancelCATToolInstall 统一为 try/finally 模式 - openCATToolInstallPage 增加 tab.id 空值检查
实现 Agent 框架的 DOM 操作 API,支持两种模式: - 默认模式:通过 chrome.scripting.executeScript 操作 - trusted 模式:通过 chrome.debugger CDP 实现真实用户输入 新增 8 个 Agent 工具:dom_list_tabs, dom_navigate, dom_read_page, dom_screenshot, dom_click, dom_fill, dom_scroll, dom_wait_for readPage 支持 summary/detail 分层读取,控制上下文大小; 操作后自动返回 ActionResult(跳转/新 tab/dialog 检测); debugger 权限放在 optional_permissions 动态申请。
- 新增 Skill 类型定义(SkillSummary, SkillRecord, SkillApiRequest 等) - SKILL.md 解析器(YAML frontmatter + markdown body) - SkillRepo OPFS 存储(registry + scripts + references) - AgentService 集成:loadSkills, installSkill, resolveSkills - 2 个 meta-tool:execute_skill_tool, read_reference - GM API: CAT.agent.skills(list/get/install/remove) - UI: ChatInput 增加 Skills 选择器
- agent-fixtures.ts: Mock LLM 基础设施(context.route 拦截 + OpenAI SSE 响应) - agent-conversation.spec.ts: 基础对话、Tool Calling、多轮上下文保持 - agent-cattool.spec.ts: CATTool 安装/调用/删除、CATTool + 对话联动
resolveSkills 不再将完整 SKILL.md 正文和 CATTool schema 一次性注入 system message, 改为三层渐进加载:1) 摘要列表 2) load_skill 按需获取 prompt 3) execute_skill_tool/read_reference 按需执行。 execute_skill_tool 新增 skill_name 参数确定工具作用域,CATTool 脚本改为执行时按需加载。 新增 14 个 Skill 系统单元测试。
ephemeral 模式下会话不持久化到 OPFS、不加载内置工具和 Skills, 工具完全由脚本提供,消息历史由脚本端内存管理。 SW 端无状态处理,仅解析 model 配置调用 LLM。
…rompts 实现完整的 MCP (Model Context Protocol) 客户端: - MCPClient: JSON-RPC 2.0 over HTTP POST,Session ID 管理,认证头 - MCPService: 连接池管理,懒连接,MCP 工具自动注册到 ToolRegistry - MCPServerRepo: chrome.storage 持久化服务器配置 - GM API: CAT.agent.mcp(SW + Content 双侧),权限验证 - UI: AgentMcp 页面 — 服务器 CRUD、启用/禁用、测试连接 - 单元测试覆盖 MCPClient、MCPToolExecutor、MCPService
agent_model:__default__ 存储的是默认模型 ID(字符串),被 find() 当作 AgentModelConfig 返回,导致 UI 访问 model.apiKey.length 时报 TypeError。
- 解析 SSE delta 中的 reasoning_content 字段,发出 thinking_delta 事件(兼容 deepseek/o-series) - 将 usage 检查移到 choices 处理之后,修复最后一个 chunk 同时包含 tool_call 增量和 usage 时丢失数据的问题 - Service Worker callLLM() 收集 thinking 内容并持久化到 assistant 消息 - 修复 vitest.config.ts 中重复 exclude 导致 e2e 测试被误执行的问题
- 注册 installSkill/removeSkill 消息处理到 AgentService.init() - AgentClient 新增 installSkill/removeSkill 方法 - 新建 AgentSkills.tsx:Skill 列表卡片、详情编辑弹窗、URL/粘贴安装弹窗 - 替换 /agent/skills 路由为实际管理页面 - 8 个语言文件新增 19 个 agent_skills_* 翻译 key - 新增 9 个单元测试覆盖安装/卸载/消息注册核心流程
新增 parseSkillZip 解析 ZIP 包(SKILL.md + tools/*.js + references/*), UI 增加 ZIP 上传 Tab,支持嵌套目录结构,含完整单元测试和端到端集成测试。
chrome.permissions.request() 在 MV3 Service Worker 中无法调用(需要用户手势), 改为打开 confirm.html 确认页面,用户点击授权按钮后通过 runtime.sendMessage 通知 SW。
将三层 meta-tool 设计(load_skill → execute_skill_tool → CATTool)简化为两层:
- load_skill 调用时动态注册该 skill 的 CATTool 为独立工具({skill}__{tool} 格式)
- callLLMWithToolLoop 每轮重新获取工具定义以发现新注册的工具
- 对话结束后清理动态注册的工具和 meta-tools
- 新增 prefixToolDefinition 辅助函数
- 新增 ServerDetailDrawer:展示 Tools/Resources/Prompts 三大能力 - 卡片操作栏增加"详情"按钮,通过 MCPClient 直接查询服务器能力 - 测试连接按钮增加 loading 状态 - 编辑 Modal 底部增加测试连接按钮,填完表单可直接测试 - 补充 8 个 locale 文件的 i18n key
为后续 AgentService 拆分铺路: - LLMProvider 抽象接口 + registry 机制(解耦 OpenAI/Anthropic) - ToolRegistry 引入来源追踪(builtin/mcp/skill/script)+ scoped 注册 - HTML 搜索引擎插件化(html_extractor 396 → 332 行) - 拆分 3296 行 agent.test.ts 为 8 个主题测试文件
将 2534 行的 AgentService 上帝对象拆分为 11 个专用服务 (AgentService 472 行,-81%),并重组 Agent 代码目录。 提取的服务: - SkillService: ZIP 安装/刷新/解析 - AgentTaskService: 定时任务 CRUD + 执行 - AgentModelService: 模型配置与选择 - AgentOPFSService: OPFS 文件访问 - SubAgentService: 子代理会话管理 - BackgroundSessionManager: 后台会话状态 - CompactService: 对话压缩摘要 - LLMClient: LLM 调用封装 - ToolLoopOrchestrator: 工具循环编排 - ChatService: 对话处理(ephemeral/compact/persistent) - resolveAttachments utility 抽取 目录重组: - src/app/service/agent/core/ 跨上下文共享核心 - src/app/service/agent/service_worker/ SW 端有状态服务 - 清理文件 agent 前缀(agent_dom.ts → dom.ts 等)
AgentTaskRun 原本存在 chrome.storage.local(键前缀 agent_task_run:),
存在几个问题:配额压力、listRuns 全表扫描、缓存常驻内存。
改为 OPFS 后:
- 存储路径:agents/task_runs/{taskId}.json(内含 run[] 降序)
- listRuns O(全表) → O(单文件 parse)
- 环形缓冲:每任务保留最近 100 条(append 时自动裁剪)
- Agent 体系存储后端统一到 OPFS
改动:
- agent_task.ts: AgentTaskRunRepo extends Repo → extends OPFSRepo
- task_scheduler.ts: updateRun 签名加 taskId 参数
- 测试:添加 OPFS mock + 2 个新用例(环形缓冲 / 更新不存在 id)
Agent 功能尚未发布,无需考虑旧数据迁移。
Typecheck 0 错误,1736 测试全绿(+2 新用例)
- 移除未使用的导入(AgentTask、sendMessage、ToolSource、部分 test helpers) - html_extractor.ts: inline import() 类型改为顶部 import type - prettier 格式化修复(with_timeout.ts、web_search.ts 等多处)
package.json 限制 sideEffects 到 CSS 文件,生产构建时 rspack 会
tree-shake 纯副作用导入。原本 providers/index.ts 用
\`import "./openai"\` 的副作用导入触发 provider 注册,在生产
bundle 中被剥离,导致运行时报 "Unsupported LLM provider: openai"。
修复:把内置 provider 注册移到 registry.ts 模块内,与
providerRegistry 单例同模块。消费者 import providerRegistry 时
必然会触发 registry.ts 的顶层语句,注册就一定发生。
验证:
- dist bundle 现在包含 d7.register({name:"openai",...}) 和
d7.register({name:"anthropic",...})
- 6 个 agent e2e 测试全部恢复通过
- 1736/1736 单元测试通过
AgentChatRepo 是 OPFS 的无状态包装,原本通过 AgentService._repo 配合 setter 同步给 4 个子服务 (CompactService/LLMClient/ ToolLoopOrchestrator/ChatService),新增子服务容易遗漏同步。 改为模块级单例 agentChatRepo,子服务直接 import 使用,测试用 vi.hoisted + vi.mock 替换整个模块,彻底消除 setter 同步仪式。
- SessionToolRegistry 组合模式:per-session 工具注册隔离,消除并发会话工具覆盖 - excludeTools 后端强校验 + 未知 sub-agent type 抛错 - ChatStreamEvent 分类重组:LLMStreamEvent → ForwardableEvent 层次化, 消除递归 sub_agent_event 改为 subAgent? 扁平标识 - Sub-agent context OPFS 持久化,resume 时内存优先、OPFS 回退 - 清理 AgentService 测试兼容性代理 getter/setter(~45 行) - 移除 ChatService 未使用的 compactService 字段 - 提取 OPFS mock 到共享 test-helpers
- 子代理使用独立 childRegistry 替代共享父 sessionRegistry,避免工具泄漏 - system prompt 增加防注入/防数据外泄/数据指令分离安全规则 - get_tab_content 增加 URL 策略校验(assertDomUrlAllowed) - researcher 子代理移除 execute_script 工具 - Anthropic provider 对畸形 toolCall arguments 降级为空对象 - 子代理超时后追加提示信息,区分用户取消与超时 - tool loop 移除 withRetry 包装,重试逻辑下沉到 llm_client - 修正 tool call budget 描述为 rounds 而非单次调用
- 新增 navigate_tab 工具:导航已有标签页到新 URL,支持等待加载完成 - researcher 子代理增加页面只读能力(get_tab_content/open_tab/list_tabs/close_tab/navigate_tab),但不允许 DOM 交互 - 移除 get_task 和 delete_task 工具,list_tasks 改为返回完整任务信息 - 页面交互指南仅在同时拥有 get_tab_content 和 execute_script 时显示
- Skill 支持 version 字段(SkillSummary/SkillMetadata/SkillRecord) - 新增 URL 安装流程:以 SKILL.cat.md 为入口,按相对路径 fetch scripts/references - 新增更新检查机制:checkForUpdates/updateSkill,基于 semver 比较 - SKILL.cat.md 优先于 SKILL.md(parseSkillZip 兼容) - DNR 拦截 .cat.md URL,安装页自动识别并走 URL 安装流程 - SkillInstallView 展示 version 和 installUrl - AgentSkills 页面支持 URL 安装输入和批量检查更新 - 8 个语言文件新增 i18n keys - 新增单元测试覆盖 version/URL 安装/更新检查
- 提取 TokenUsage 公共类型,消除 6 处内联重复 - AgentTask 改为判别联合类型(InternalAgentTask | EventAgentTask) - zhipu 注册为正式 Provider,消除 llm_client 硬编码分支 - handleConversationChat 拆分为 5 个职责单一的子方法 - Provider 层提取 content_utils.ts 公共逻辑(SSE 读取、内容块转换、附件 ID 生成) - agentChatRepo 统一构造函数注入(LLMClient/CompactService/ToolLoop/ChatService) - skill_service 提取 fetchSkillResources 共享方法 - web_search 搜索方法模板抽取(fetchAndExtract) - 统一工具参数校验辅助函数(param_utils.ts) - 提取 UA/超时常量,tab_tools 复用 stripHtmlTags - 删除目录重组遗留的旧文件 - 修复 e2e agent-fixtures 缺失 expect 导出
- 简化 agent-chat.spec.ts:移除不可靠的 LLM mock 测试,保留基础 UI 验证 - 修复 agent-fixtures.ts 缺失的 expect 导出 - 提交 e2e/utils.ts 的 Agent 辅助函数(openAgentChatPage 等) - 删除 debug-provider 调试文件
- pre-commit:每次提交前运行 typecheck + eslint --fix - pre-push:仅推送到 main/release 时运行测试 - 修正原 pre-commit 实际为 pre-push 逻辑的问题 - 移除 lint-staged(直接全量 lint-fix)
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
该 PR 为 ScriptCat 引入完整的 AI Agent 平台能力,使用户/脚本可通过自然语言对话驱动浏览器自动化(DOM 操作、工具调用、Skill 系统、MCP 扩展、定时任务、OPFS 工作区等),并补齐了核心模块的单测与 E2E 覆盖。
Changes:
- 新增会话级工具隔离、子代理(Sub-Agent)类型系统、工具集(web_fetch/web_search/execute_script/ask_user/task_tools)、SSE 解析与 Provider 注册体系。
- 引入 OPFS 工作区与多类 Repo 持久化(对话/附件/任务运行/Skill/子代理上下文),并扩展 MCP 客户端与工具桥接。
- 新增/重构大量单元测试与 Playwright E2E fixture/用例;更新 ESLint grant 兼容表、CI 脚本与依赖。
Reviewed changes
Copilot reviewed 95 out of 216 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/service/agent/service_worker/compact_service.ts | 新增对话自动 compact 与内容摘要能力(SW 侧调用 LLM) |
| src/app/service/agent/service_worker/background_session_manager.ts | 管理后台运行会话的流式状态与 UI attach/broadcast |
| src/app/service/agent/service_worker/autocompact.test.ts | compact(手动/自动)行为测试 |
| src/app/service/agent/core/tools/web_search.ts | 新增 web_search 工具(多引擎 + Offscreen 提取) |
| src/app/service/agent/core/tools/web_fetch.ts | 新增 web_fetch 工具(抓取 + Offscreen 提取 + 可选摘要) |
| src/app/service/agent/core/tools/task_tools.ts | 新增任务管理工具集(create/update/list) |
| src/app/service/agent/core/tools/task_tools.test.ts | task_tools 单测 |
| src/app/service/agent/core/tools/sub_agent.ts | 新增子代理 tool(agent)封装 |
| src/app/service/agent/core/tools/sub_agent.test.ts | sub_agent 单测 |
| src/app/service/agent/core/tools/search_config.ts | 新增搜索引擎配置 Repo(storage.local) |
| src/app/service/agent/core/tools/param_utils.ts | 新增工具参数校验/转换 util |
| src/app/service/agent/core/tools/execute_script.ts | 新增 execute_script 工具(page/sandbox + 超时) |
| src/app/service/agent/core/tools/execute_script.test.ts | execute_script 单测 |
| src/app/service/agent/core/tools/ask_user.ts | 新增 ask_user 工具(UI 提问 + 等待回复/超时) |
| src/app/service/agent/core/tools/ask_user.test.ts | ask_user 单测 |
| src/app/service/agent/core/tool_call_guard.ts | 新增工具调用循环/重复检测与系统告警 |
| src/app/service/agent/core/task_scheduler.ts | 新增定时任务调度器(cron + run 记录) |
| src/app/service/agent/core/system_prompt.test.ts | system prompt 组装的单测补齐 |
| src/app/service/agent/core/sub_agent_types.ts | 新增子代理类型/权限白黑名单与解析逻辑 |
| src/app/service/agent/core/sub_agent_types.test.ts | sub_agent_types 单测 |
| src/app/service/agent/core/sse_parser.ts | 新增 SSE 文本流解析器 |
| src/app/service/agent/core/sse_parser.test.ts | SSEParser 单测 |
| src/app/service/agent/core/skill_script_executor.ts | 新增 SkillScript 执行器(Offscreen->Sandbox + 超时 + grants 映射) |
| src/app/service/agent/core/session_tool_registry.ts | 新增会话级 ToolRegistry(隔离并发会话闭包污染) |
| src/app/service/agent/core/providers/types.ts | 新增 Provider 抽象接口类型 |
| src/app/service/agent/core/providers/registry.ts | 新增 ProviderRegistry,并注册 openai/anthropic/zhipu |
| src/app/service/agent/core/providers/registry.test.ts | ProviderRegistry 单测 |
| src/app/service/agent/core/providers/openai.ts | OpenAI 兼容请求构建与流解析(含 tool_calls/图片块) |
| src/app/service/agent/core/providers/index.ts | providers 导出入口(registry 单例) |
| src/app/service/agent/core/providers/content_utils.ts | Provider 侧 SSE 读取公共骨架与块转换辅助 |
| src/app/service/agent/core/opfs_helpers.ts | 新增 OPFS workspace 辅助(sanitizePath/写文件/dataURL 解码) |
| src/app/service/agent/core/model_context.ts | 新增模型上下文窗口推断/覆盖逻辑 |
| src/app/service/agent/core/model_context.test.ts | model_context 单测 |
| src/app/service/agent/core/mcp_tool_executor.ts | MCP 工具执行器桥接(图片内容转附件) |
| src/app/service/agent/core/mcp_tool_executor.test.ts | MCPToolExecutor 单测 |
| src/app/service/agent/core/mcp_client.ts | MCP JSON-RPC over HTTP 客户端实现 |
| src/app/service/agent/core/content_utils.ts | MessageContent/ContentBlock 统一处理与 MIME 扩展名工具 |
| src/app/service/agent/core/content_utils.test.ts | content_utils 单测 |
| src/app/service/agent/core/compact_prompt.ts | compact 提示词与 提取逻辑 |
| src/app/service/agent/core/compact_prompt.test.ts | compact_prompt 单测 |
| src/app/service/agent/core/attachment_resolver.ts | 解析消息中的 vision 图片附件为 data URL(供 provider) |
| src/app/repo/test-helpers.ts | 抽取可复用的 OPFS mock(供 repo 测试) |
| src/app/repo/sub_agent_context.ts | 新增子代理上下文 OPFS Repo(LRU 上限) |
| src/app/repo/sub_agent_context.test.ts | SubAgentContextRepo 单测 |
| src/app/repo/skill_repo.ts | 新增 Skill OPFS Repo(registry + data/scripts/references/config) |
| src/app/repo/opfs_repo.ts | 新增 OPFSRepo 基类(JSON 读写/列文件/删目录) |
| src/app/repo/mcp_server_repo.ts | 新增 MCP server 配置 Repo(storage.local) |
| src/app/repo/agent_task.ts | 新增定时任务 Repo(storage + run OPFS 保存) |
| src/app/repo/agent_task.test.ts | AgentTaskRepo/RunRepo 单测 |
| src/app/repo/agent_model.ts | 新增模型配置 Repo(storage + default/summary key) |
| src/app/repo/agent_chat.ts | 新增对话/消息/附件/会话任务 OPFS Repo(含旧路径兼容) |
| src/app/repo/agent_chat.test.ts | AgentChatRepo 附件路径/清理等单测 |
| src/app/cache_key.ts | 新增 Skill 安装缓存 key |
| packages/eslint/linter-config.ts | ESLint globals 增加 CAT |
| packages/eslint/compat-grant.js | 增加 CAT.agent.* grant 兼容映射 |
| package.json | 更新测试脚本参数与新增 markdown/highlight 相关依赖,调整 uglify-js 位置 |
| example/agent/README.md | Agent 示例迁移到独立 skills 仓库说明 |
| e2e/utils.ts | 抽取 E2E 通用工具(权限自动确认、脚本注入、Agent 页面打开、SSE 构建等) |
| e2e/options.spec.ts | 稳定化侧边栏定位,避免误点子菜单 |
| e2e/gm-api.spec.ts | 复用 fixtures/utils,删除重复逻辑 |
| e2e/fixtures.ts | 增加两阶段 userScripts fixture + proxy 支持 |
| e2e/agent-skill.spec.ts | 新增 Skill 安装/加载/执行的集成 E2E |
| e2e/agent-provider.spec.ts | 新增 Provider 配置 UI E2E |
| e2e/agent-navigation.spec.ts | 新增 Agent 路由导航 E2E |
| e2e/agent-fixtures.ts | Agent 专用两阶段 fixture + mock LLM route |
| e2e/agent-error-handling.spec.ts | 新增错误处理/重试相关 E2E |
| e2e/agent-conversation.spec.ts | 新增 conversation API 的 E2E(多轮/工具调用) |
| e2e/agent-chat.spec.ts | 新增 Agent Chat 页面基础 E2E |
| .husky/pre-push | 新增按分支触发的 pre-push 测试钩子 |
| .husky/pre-commit | 精简为 lint-fix,并支持 SKIP_PRE_COMMIT |
| .github/workflows/test.yaml | workflow 触发分支调整(移除 feature/*) |
| .github/workflows/build.yaml | workflow 触发分支调整(移除 feature/*) |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| this.currentEvent = ""; | ||
| this.currentData = []; | ||
| } |
There was a problem hiding this comment.
"空行结束事件"时只在 currentData.length>0 才重置 currentEvent/currentData,会导致出现仅包含 event 字段但没有 data 的事件后,currentEvent 残留并污染下一条事件类型(下一条没有 event 字段时会错误继承上一条 event)。建议在遇到空行时无条件重置 currentEvent/currentData;是否要在无 data 时也产出事件可按你们协议决定,但至少要清理状态。
| this.currentEvent = ""; | |
| this.currentData = []; | |
| } | |
| } | |
| this.currentEvent = ""; | |
| this.currentData = []; |
| if (data instanceof Blob) { | ||
| await writable.write(data); | ||
| } else if (data instanceof Uint8Array) { | ||
| await writable.write(data.buffer as ArrayBuffer); |
There was a problem hiding this comment.
这里对 Uint8Array 直接写入 data.buffer 会忽略 byteOffset/byteLength:当传入的是某个 ArrayBuffer 的“视图切片”时,会把整个底层 buffer 都写入,产生错误内容/额外字节。建议写入一个精确切片的 ArrayBuffer(例如按 byteOffset/byteLength 截取),或直接写入 Uint8Array 本身(若 OPFS writable 支持)。
| await writable.write(data.buffer as ArrayBuffer); | |
| await writable.write(data); |
| async execute(args: Record<string, unknown>): Promise<string> { | ||
| const query = requireString(args, "query"); | ||
| const maxResults = Math.min((args.max_results as number) || 5, 10); |
There was a problem hiding this comment.
max_results 这里直接用类型断言读 args.max_results,若传入无法转为数值的字符串(例如 "abc"),Math.min 会得到 NaN,最终 results.slice(0, NaN) 返回空数组,行为与“默认 5”不一致。建议复用 param_utils.optionalNumber 做稳健解析,并在 NaN/<=0 时回退到默认值。
| /** 格式化搜索结果,区分"无结果"和"提取失败" */ | ||
| function formatSearchResults( | ||
| results: Array<{ title: string; url: string; snippet: string }>, | ||
| extractionFailed: boolean, | ||
| engine: string | ||
| ): string { | ||
| if (extractionFailed && results.length === 0) { | ||
| return JSON.stringify({ | ||
| results: [], | ||
| warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`, | ||
| }); | ||
| } | ||
| return JSON.stringify(results); | ||
| } | ||
|
|
||
| /** 搜索结果条目类型 */ | ||
| type SearchResult = { title: string; url: string; snippet: string }; | ||
|
|
There was a problem hiding this comment.
工具返回值在“提取失败”时是对象({results, warning}),正常时却是数组([])。这会让上层解析变得分支化且容易出错。建议统一返回结构(例如始终返回 { results: SearchResult[], warning?: string }),即使正常也包一层 results,warning 可选。
| /** 格式化搜索结果,区分"无结果"和"提取失败" */ | |
| function formatSearchResults( | |
| results: Array<{ title: string; url: string; snippet: string }>, | |
| extractionFailed: boolean, | |
| engine: string | |
| ): string { | |
| if (extractionFailed && results.length === 0) { | |
| return JSON.stringify({ | |
| results: [], | |
| warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`, | |
| }); | |
| } | |
| return JSON.stringify(results); | |
| } | |
| /** 搜索结果条目类型 */ | |
| type SearchResult = { title: string; url: string; snippet: string }; | |
| /** 搜索结果条目类型 */ | |
| type SearchResult = { title: string; url: string; snippet: string }; | |
| /** 格式化搜索结果,区分"无结果"和"提取失败",并统一返回结构 */ | |
| function formatSearchResults(results: SearchResult[], extractionFailed: boolean, engine: string): string { | |
| return JSON.stringify({ | |
| results, | |
| ...(extractionFailed && results.length === 0 | |
| ? { | |
| warning: `Result extraction failed or timed out (engine: ${engine}). Try a different search engine or rephrase the query.`, | |
| } | |
| : {}), | |
| }); | |
| } |
|
|
||
| async execute(args: Record<string, unknown>): Promise<string> { | ||
| const url = requireString(args, "url"); | ||
| const prompt = args.prompt as string | undefined; |
There was a problem hiding this comment.
WEB_FETCH_DEFINITION 声明 prompt 为 required,但实现里没有对 prompt 做 requireString 校验,导致调用方可以绕过“必须提供 prompt”的约束(并且 summarize 分支不会触发,可能返回大段原文、增加上下文压力)。建议与 schema 对齐:对 prompt 做必填校验(至少非空字符串),或同步调整 schema。
| const prompt = args.prompt as string | undefined; | |
| const prompt = requireString(args, "prompt").trim(); | |
| if (!prompt) { | |
| throw new Error("Parameter 'prompt' must be a non-empty string"); | |
| } |
| definition: ToolDefinition; | ||
| executor: ToolExecutor; | ||
| } { | ||
| const timeoutMs = deps.timeoutMs ?? EXECUTE_SCRIPT_TIMEOUT_MS; |
There was a problem hiding this comment.
这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。
| const { result, tabId: actualTabId } = await withTimeout( | ||
| deps.executeInPage(code, { tabId }), | ||
| timeoutMs, | ||
| () => new Error("execute_script timed out after 30s") | ||
| ); |
There was a problem hiding this comment.
这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。
| const result = await withTimeout( | ||
| deps.executeInSandbox(code), | ||
| timeoutMs, | ||
| () => new Error("execute_script timed out after 30s") | ||
| ); |
There was a problem hiding this comment.
这里超时错误消息硬编码为 "after 30s",但实际超时由 timeoutMs 决定(测试里会覆盖为 50ms)。建议错误消息使用 timeoutMs 动态生成(例如换算成秒/毫秒),避免排障时“看见 30s 但实际更短/更长”的误导。
| import { supportsVision } from "@App/pages/options/routes/AgentChat/model_utils"; | ||
|
|
There was a problem hiding this comment.
core 模块依赖 pages/options 下的 UI 路径(supportsVision)会增加跨层耦合,且更容易引入打包体积/循环依赖风险(尤其在 service worker 侧复用时)。建议将 supportsVision 下沉到 agent/core 内的纯函数工具(或共享 util 包)再由 UI 侧复用,保证 core 侧无 UI 依赖。
| import { supportsVision } from "@App/pages/options/routes/AgentChat/model_utils"; | |
| function supportsVision(model: AgentModelConfig): boolean { | |
| const candidate = model as AgentModelConfig & { | |
| supportsVision?: boolean; | |
| supportVision?: boolean; | |
| vision?: boolean; | |
| capabilities?: { | |
| vision?: boolean; | |
| image?: boolean; | |
| }; | |
| modalities?: string[]; | |
| }; | |
| if (typeof candidate.supportsVision === "boolean") { | |
| return candidate.supportsVision; | |
| } | |
| if (typeof candidate.supportVision === "boolean") { | |
| return candidate.supportVision; | |
| } | |
| if (typeof candidate.vision === "boolean") { | |
| return candidate.vision; | |
| } | |
| if (candidate.capabilities) { | |
| if (typeof candidate.capabilities.vision === "boolean") { | |
| return candidate.capabilities.vision; | |
| } | |
| if (typeof candidate.capabilities.image === "boolean") { | |
| return candidate.capabilities.image; | |
| } | |
| } | |
| if (Array.isArray(candidate.modalities)) { | |
| return candidate.modalities.includes("vision") || candidate.modalities.includes("image"); | |
| } | |
| return false; | |
| } |
| onDisconnect: vi.fn(), | ||
| }; | ||
| const sender = { | ||
| isType: (type: any) => type === 1, |
There was a problem hiding this comment.
测试里用 magic number (type === 1) 模拟 CONNECT sender,会让用例对 GetSenderType.CONNECT 的具体枚举值产生隐式依赖(未来枚举变更会导致测试“无意义失败”)。建议直接引入并使用 GetSenderType.CONNECT 来比较,或在 helper 里参数化该值。
| isType: (type: any) => type === 1, | |
| isType: vi.fn(() => true), |
- SSE 解析器空行时无条件重置 currentEvent,防止残留污染 - OPFS 写入 Uint8Array 时精确截取字节段,修复切片视图 bug - execute_script 超时消息改为动态生成,匹配实际超时时间 - cleanupIfDone 回调中重新检查会话状态,防止误删已复用会话 - 模型能力检测函数移至 core/model_capabilities.ts,消除 core→UI 反向依赖 - web_search max_results 改用 optionalNumber 防止 NaN - task_tools subject/description 使用类型安全的参数校验
agent 工具新增 tab_id 可选参数,父代理可将已打开的标签页传递给子代理, 子代理会直接使用该标签页而非重复打开新页面。
pre-commit:lint + main/release/* 分支跑 test:ci 删除 pre-push hook
|
@CodFrm 我尝试用 skills 给AI看这个PR 它好像找到了问题。你可以研究一下 📋 PR 概要 / PR SummaryPR: #1324 · 💥 脚本猫 AI Agent 🔍 变更分析 / Change Analysis整体架构这是一个从零构建的完整 AI Agent 平台。核心分层清晰: 重要决策是将持久化统一到 OPFS(对话、附件、任务记录、Skill、子代理上下文),只有轻量配置留在 核心模块逐一评估
DOM 操作 / CDP 双引擎 测试覆盖
|

概述
为 ScriptCat 新增完整的 AI Agent 平台,用户可以通过自然语言对话控制浏览器,自动完成网页操作、定时任务、工具调用等复杂工作流。
构建产物可以从 https://github.com/scriptscat/scriptcat/actions/workflows/build.yaml feature/agent 获取
Youtube 演示
https://www.youtube.com/watch?v=6OT7qeY3Uuk
如果你对此感兴趣,可以加入 Discord 进行交流
核心功能
🤖 AI 对话与多模型支持
🌐 浏览器 DOM 操作
🧩 Skill 技能系统
🔧 CATTool 自定义工具
🔌 MCP 协议支持
⏰ 定时任务调度
📂 OPFS 文件系统
💬 GM API 扩展(脚本侧 Agent API)
CAT.agent.*系列 API,用户脚本可与 Agent 深度交互:CAT.agent.dom— 脚本内调用 DOM 操作CAT.agent.conversation— 管理对话上下文CAT.agent.tools— 动态注册/调用工具CAT.agent.task— 任务生命周期管理UI 页面
技术亮点
数据规模